package ia.robot.app;

import ia.robot.graphics.Textures;
import ia.robot.gui.MainCanvasPane;
import ia.robot.gui.MainFrame;
import ia.robot.gui.MainLogPane;

import java.util.ArrayList;
import java.util.LinkedList;

/**
 * Clase que aplica las estragegias de busqueda para guiar un robot hasta su
 * destino.
 * 
 * @author Jaime A. Corrales Gonzalez
 * @author Alejandro Tejera Perez
 */
public class Robot extends Thread {

	public static final String[] searchMethodStrings = { "Depth",
			"Hill climbing" };

	public static final String[] heuristicStrings = { "Euclidean",
			"Mahalanobis", "Manhattan" };

	public static final int DEPTH_SEARCH = 1;
	public static final int HILL_CLIMBING_SEARCH = 2;

	public static final int EUCLIDEAN_DISTANCE = 1;
	public static final int MAHALANOBIS_DISTANCE = 2;
	public static final int MANHATTAN_DISTANCE = 3;

	private ArrayList<Point> visitedPoints;
	private LinkedList<Point> path;

	private Board board;

	private MainCanvasPane canvas;
	private MainFrame frame;
	private MainLogPane logger;

	private Options options;

	private int stepCount;
	private int stepBackwardCount;
	private int stepForwardCount;
	private boolean solutionFound;

	private double averageDepthSteps;
	private double averageDepthPathSteps;
	private double averageEuclideanSteps;
	private double averageEuclideanPathSteps;
	private double averageMahalanobisSteps;
	private double averageMahalanobisPathSteps;
	private double averageManhattanSteps;
	private double averageManhattanPathSteps;

	public Robot(Board b, MainCanvasPane mcp, MainLogPane mlp, MainFrame mf,
			Options o) {
		visitedPoints = new ArrayList<Point>();
		path = new LinkedList<Point>();

		board = b;
		canvas = mcp;
		frame = mf;
		logger = mlp;
		options = o;

		stepCount = 0;
		stepBackwardCount = 0;
		stepForwardCount = 0;
		solutionFound = false;
		
		resetAverages();
	}

	// Getters

	public LinkedList<Point> getPath() {
		return path;
	}

	public int getStepCount() {
		return stepCount;
	}

	public int getSolutionStepCount() {
		return path.size() - 1;
	}

	public boolean isSolved() {
		return solutionFound;
	}
	
	public double getAverageEuclideanSteps() {
		return averageEuclideanSteps;
	}

	public double getAverageMahalanobisSteps() {
		return averageMahalanobisSteps;
	}

	public double getAverageManhattanSteps() {
		return averageManhattanSteps;
	}
	
	public void setNewBoard (Board b) {
		this.board = b;
	}
	
	public double getAverageEuclideanPathSteps() {
		return averageEuclideanPathSteps;
	}

	public double getAverageMahalanobisPathSteps() {
		return averageMahalanobisPathSteps;
	}

	public double getAverageManhattanPathSteps() {
		return averageManhattanPathSteps;
	}
	
	public double getAverageDepthPathSteps() {
		return averageDepthPathSteps;
	}

	public double getAverageDepthSteps() {
		return averageDepthSteps;
	}

	// Funciones de busqueda
	/**
	 * Metodo que expande el primer nodo ortogonalmente adyacente que no haya
	 * sido visitado.
	 * 
	 * @param p
	 *            - Nodo origen.
	 * @return True si se ha expandido un nodo. False en caso contrario.
	 */
	private boolean insertFirstSon(Point p) {
		Point newPoint = null;

		// Para cada una de las cuatro direcciones ortogonales
		for (int i = 0; i <= 3; i++) {

			// Obtenemos el nodo resultante de aplicar un desplazamiento al nodo actual
			newPoint = p.displace(i);

			// Si dicho nodo no ha sido expandido y es viable para el transito
			if ((!visitedPoints.contains(newPoint)) && (board.getContent(newPoint) != Textures.TILE_STONE)) {
				// Lo anadimos al camino y a la lista de nodos expandidos.
				path.addLast(newPoint);
				visitedPoints.add(newPoint);

				// Actualizamos la posicion del robot y su orientacion
				board.setBotPos(newPoint);
				board.setBotDir(i);

				stepCount++;
				stepForwardCount++;

				refreshCanvas();

				return true;
			}
		}

		return false;
	}
	
	private boolean insertClosestSon (Point p) {
		Point newPoint = null;
		Point best = null;
		int bestDir = -1;
		for (int i = 0; i <= 3; i++) {
			newPoint = p.displace(i);
			if ((!visitedPoints.contains(newPoint)) && (board.getContent(newPoint) != Textures.TILE_STONE)) {
				if (best == null) {
					best = newPoint;
					bestDir = i;
				}
				else
					switch (options.getHeuristic()) {
						case EUCLIDEAN_DISTANCE:
							if (best.euclideanDistance(board.getDestPos()) > newPoint.euclideanDistance(board.getDestPos())) {
								best = newPoint;
								bestDir = i;
							}
							break;
						case MANHATTAN_DISTANCE: 						
							if (best.manhattanDistance(board.getDestPos()) > newPoint.manhattanDistance(board.getDestPos())) {
								best = newPoint;
								bestDir = i;
							}
							break;
						case MAHALANOBIS_DISTANCE: 						
							if (best.mahalanobisDistance(board.getDestPos()) > newPoint.mahalanobisDistance(board.getDestPos())) {
								best = newPoint;
								bestDir = i;
						}
					}
				
			}
		}
		if (best != null) {
			// Lo anadimos al camino y a la lista de nodos expandidos.
			path.addLast(best);
			visitedPoints.add(best);

			// Actualizamos la posicion del robot y su orientacion
			board.setBotPos(best);
			board.setBotDir(bestDir);

			stepCount++;
			stepForwardCount++;

			return true;
		}
		return false;
	}

	/**
	 * Metodo que aplica una estrategia en profundidad.
	 * 
	 * @return True si ha llegado al destino. False en caso contrario.
	 */
	public boolean depthSearch() {
		reset();
		path.addFirst(board.getBotPos());
		visitedPoints.add(board.getBotPos());

		while (true) {

			// En caso de no tratarse de la posicion destino
			if (!path.getLast().equals(board.getDestPos())) {

				// Si no hay hijos no visitados para expandir
				if (!insertFirstSon(path.getLast())) {
					path.removeLast();

					// En el caso de que no podamos expandir mas nodos
					if (path.isEmpty())
						return false;

					// Actualizamos la posicion del robot y su orientacion
					board.setBotDir(board.getBotPos().displace(path.getLast()));
					board.setBotPos(path.getLast());

					stepCount++;
					stepBackwardCount++;

					refreshCanvas();
				}

			} else
				// Si no, hemos alcanzado el destino
				return true;
		}
	}

	/**
	 * Metodo que aplica una estrategia en escalada.
	 * 
	 * @return True si ha llegado al destino. False en caso contrario.
	 */
	public boolean hillClimbingSearch() {
		reset();
		path.addFirst(board.getBotPos());
		visitedPoints.add(board.getBotPos());
		while (true) {

			// En caso de no tratarse de la posicion destino
			if (!path.getLast().equals(board.getDestPos())) {

				// Si no hay hijos no visitados para expandir
				if (!insertClosestSon(path.getLast())) {
					path.removeLast();

					// En el caso de que no podamos expandir mas nodos
					if (path.isEmpty())
						return false;

					// Actualizamos la posicion del robot y su orientacion
					board.setBotDir(board.getBotPos().displace(path.getLast()));
					board.setBotPos(path.getLast());

					stepCount++;
					stepBackwardCount++;

					refreshCanvas();
				}

			} else
				// Si no, hemos alcanzado el destino
				return true;
		}	}

	// Funciones de thread.

	public void run() {
		// Bloquear la interfaz
		frame.lockGUI();

		switch (options.getSearchMethod()) {
		case DEPTH_SEARCH:
			solutionFound = depthSearch();
			break;
		case HILL_CLIMBING_SEARCH:
			solutionFound = hillClimbingSearch();
			break;
		default:
			solutionFound = false;
		}

		logger.write("A path " + (solutionFound ? "was" : "wasn't")
				+ " found.\n");
		logger.write("  Steps taken: " + stepCount + " steps ("
				+ stepForwardCount + " forward, " + stepBackwardCount
				+ " backwards).\n");

		if (solutionFound) {
			logger.write("  Steps of path: " + (path.size() - 1) + " steps.\n");

			canvas.setPath(path);
			canvas.blitBitmap(board.getBitmap());
		} else
			canvas.setPath(null);

		logger.write("\n");

		// Desbloquear la interfaz
		frame.unlockGUI();
	}

	// Funciones utiles.

	/**
	 * Metodo que repinta el tablero con informacion actualizada.
	 */
	private void refreshCanvas() {
		if (options.getDelay() != 0) {
			try {
				Thread.sleep(options.getDelay());
			} catch (InterruptedException e) {

			}

			canvas.blitBitmap(board.getBitmap());
		}
	}

	/**
	 * Metodo que reinicializa las estructuras de datos y variables necesarias
	 * para aplicar una estrategia.
	 */
	public void reset() {
		visitedPoints.clear();
		path.clear();

		stepCount = 0;
		stepBackwardCount = 0;
		stepForwardCount = 0;
		solutionFound = false;
		
	}
	
	public void resetAverages () {
		this.averageDepthSteps = 0;
		this.averageDepthPathSteps = 0;
		this.averageEuclideanPathSteps = 0;
		this.averageEuclideanSteps = 0;
		this.averageMahalanobisPathSteps = 0;
		this.averageMahalanobisSteps = 0;
		this.averageManhattanPathSteps = 0;
		this.averageManhattanSteps = 0;
	}
	
	public void test() {
		refreshCanvas();
		Point start = board.getBotPos();
		Point end = board.getDestPos();
		
		this.depthSearch();
		if (averageDepthSteps == 0) {
			averageDepthSteps = stepCount;
			averageDepthPathSteps = path.size() - 1;
		} else {
			averageDepthSteps = (averageDepthSteps + stepCount) / 2;
			averageDepthPathSteps = (averageDepthPathSteps + (path.size() - 1)) / 2;
		}
		
		board.setBotPos(start);
		board.setDestPos(end);
		for (int i = 1; i <= 3; i++) {
			this.options.setHeuristic(i);
			
			this.hillClimbingSearch();
			
			switch (i) {
			case EUCLIDEAN_DISTANCE:
				if (averageEuclideanSteps == 0) {
					averageEuclideanSteps = stepCount;
					averageEuclideanPathSteps = path.size() - 1;
				} else {
					averageEuclideanSteps = (averageEuclideanSteps + stepCount) / 2;
					averageEuclideanPathSteps = (averageEuclideanPathSteps + (path.size() - 1)) / 2;
				}
				break;
			case MAHALANOBIS_DISTANCE:
				if (averageMahalanobisSteps == 0) {
					averageMahalanobisSteps = stepCount;
					averageMahalanobisPathSteps = path.size() - 1;
				} else {
					averageMahalanobisSteps = (averageMahalanobisSteps + stepCount) / 2;
					averageMahalanobisPathSteps = (averageMahalanobisPathSteps + (path.size() - 1)) / 2;
				}
				break;
			case MANHATTAN_DISTANCE:
				if (averageManhattanSteps == 0) {
					averageManhattanSteps = stepCount;
					averageManhattanPathSteps = path.size() - 1;
				} else {
					averageManhattanSteps = (averageManhattanSteps + stepCount) / 2;
					averageManhattanPathSteps = (averageManhattanPathSteps + (path.size() - 1)) / 2;
				}
				break;
			}

			board.setBotPos(start);
			board.setDestPos(end);
		}
		
	}
}
